package session

import (
	"context"
	"log"
	"mi/compiler/ast"
	"mi/compiler/binder"
	"mi/compiler/checker"
	"mi/compiler/scanner"
	"mi/compiler/token"
	"mi/internal/lsp/protocol"
	"mi/internal/lsp/snippet"
	"mi/internal/span"
	"mi/internal/system"
	errors "mi/internal/xerrors"
	"mi/internal/xpath"
	"unicode/utf8"
)

var keywordCompletions []CompletionItem

func init() {
	for i := token.FirstKeyword; i < token.LastKeyword; i++{
		name := i.ToString()
		keywordCompletions = append(keywordCompletions, CompletionItem{
			Label:      name,
			InsertText: name,
			Kind:       SEKKeyword,
		})
	}
}

type completionData struct {
	isRightOfDot bool
	isGlobalCompletion bool
	isMemberCompletion bool
	isNewIdentifierLocation bool
	symbols []*binder.Symbol
}

type getCompletionDataContext struct {
	typeChecker checker.TypeChecker
	sourceFile *ast.SourceFile
	position int
	node ast.Node
	previousToken ast.Node
	contextToken ast.Node
	result completionData
}

func getCompletionData(typeChecker checker.TypeChecker, sourceFile *ast.SourceFile, position int) *completionData {
	var ctx = &getCompletionDataContext{
		typeChecker: typeChecker,
		sourceFile:  sourceFile,
		position:    position,
	}

	var start = system.CurrentMillisecond()
	var currentToken = getTokenAtPosition(sourceFile, position, false)
	log.Printf("getCompletionData: Get current token: %d\n", system.CurrentMillisecond() - start)

	start = system.CurrentMillisecond()
	var insideComment = isInsideComment(sourceFile, currentToken, position)
	log.Printf("getCompletionData: Is inside comment: %d\n", system.CurrentMillisecond() - start)
	if insideComment {
		return nil
	}

	ctx.previousToken = findPrecedingToken(position, sourceFile, nil)
	ctx.contextToken = ctx.previousToken
	if ctx.contextToken != nil && position <= ctx.contextToken.End() && isWord(ctx.contextToken) {
		ctx.contextToken = findPrecedingToken(getNodeFullStart(ctx.contextToken), sourceFile, nil)
	}

	ctx.node = currentToken
	ctx.result.isRightOfDot = false

	if ctx.contextToken != nil {
		var parent = ctx.contextToken.Parent()
		if ast.IsTokenNodeAndEqualKind(ctx.contextToken, token.Dot) {
			switch pn := parent.(type) {
			case *ast.SelectorExpression:
				ctx.node = pn.Expression
				ctx.result.isRightOfDot = true
			case *ast.QualifiedName:
				ctx.node = pn.Left
				ctx.result.isRightOfDot = true
			default:
				// There is nothing that precedes the dot, so this likely just a stray character
				// or leading into a '...' token. Just bail out instead.
				return nil
			}
		}
	}

	ctx.result.isGlobalCompletion = false
	ctx.result.isMemberCompletion = false
	ctx.result.isNewIdentifierLocation = false

	if ctx.result.isRightOfDot {
		getTypeScriptMemberSymbols(ctx)
	} else {
		tryGetGlobalSymbols(ctx)
	}

	return &ctx.result
}

func getTypeScriptMemberSymbols(ctx *getCompletionDataContext) {
	ctx.result.isGlobalCompletion = false
	ctx.result.isMemberCompletion = true
	ctx.result.isNewIdentifierLocation = false

	t := ctx.typeChecker.GetTypeOfNode(ctx.node)
	addTypeProperties(ctx, t)
}

func addTypeProperties(ctx *getCompletionDataContext, t checker.Type) {
	if t != nil {
		// Filter private properties
		for _, symbol := range ctx.typeChecker.GetAugmentedPropertiesOfApparentType(t) {
			ctx.result.symbols = append(ctx.result.symbols, symbol)
		}
	}
}

func tryGetGlobalSymbols(ctx *getCompletionDataContext) bool {
	// Get all entities in the current scope.
	ctx.result.isMemberCompletion = false
	ctx.result.isNewIdentifierLocation = false

	if ctx.previousToken != ctx.contextToken {
		if ctx.previousToken == nil {
			panic("Expected 'contextToken' to be defined when different from 'previousToken'.")
		}
	}
	// We need to find the ast that will give us an appropriate scope to begin
	// aggregating completion candidates. This is achieved in 'getScopeNode'
	// by finding the first ast that encompasses a offset, accounting for whether a ast
	// is "complete" to decide whether a offset belongs to the ast.
	//
	// However, at the end of an identifier, we are interested in the scope of the identifier
	// itself, but fall outside of the identifier. For instance:
	//
	//      xyz => x$
	//
	// the cursor is outside of both the 'x' and the arrow function 'xyz => x',
	// so 'xyz' is not returned in our results.
	//
	// We define 'adjustedPosition' so that we may appropriately account for
	// being at the end of an identifier. The intention is that if requesting completion
	// at the end of an identifier, it should be effectively equivalent to requesting completion
	// anywhere inside/at the beginning of the identifier. So in the previous case, the
	// 'adjustedPosition' will work as if requesting completion in the following:
	//
	//      xyz => $x
	//
	// If previousToken !== contextToken, then
	//   - 'contextToken' was adjusted to the token prior to 'previousToken'
	//      because we were at the end of an identifier.
	//   - 'previousToken' is defined.
	var adjustedPosition int
	if ctx.previousToken != ctx.contextToken {
		adjustedPosition = getNodeStart(ctx.previousToken, nil, false)
	} else {
		adjustedPosition = ctx.position
	}

	var scopeNode = getScopeNode(ctx.contextToken, adjustedPosition, ctx.sourceFile)
	if scopeNode == nil {
		scopeNode = ctx.sourceFile
	}

	if scopeNode != nil {
		ctx.result.isGlobalCompletion = ast.IsSourceFile(scopeNode) || ast.IsStatement(scopeNode)
	}

	/// TODO filter meaning based on the current context
	var symbolMeanings = binder.Type|binder.Value|binder.Namespace|binder.MethodLike
	ctx.result.symbols = ctx.typeChecker.GetSymbolsInScope(scopeNode, symbolMeanings)
	return true
}


// Finds the first ast that "embraces" the offset, so that one may accurately
// aggregate locals from the closest containing scope. stop at source file.
func getScopeNode(initialToken ast.Node, position int, sourceFile *ast.SourceFile) ast.Node {
	var scope = initialToken
	for !isNull(scope) && !positionBelongsToNode(scope, position, sourceFile) && !ast.IsSourceFile(scope) {
		scope = scope.Parent()
	}
	return scope
}

type CompletionItem struct {
	// Label is the primary text the user sees for this completion item.
	Label string

	// Detail is supplemental information to present to the user.
	// This often contains the type or return type of the completion item.
	Detail string

	// InsertText is the text to insert if this item is selected.
	// Any of the prefix that has already been typed is not trimmed.
	// The insert text does not contain snippets.
	InsertText string

	Kind ScriptElementKind

	Range protocol.Range

	// An optional array of additional TextEdits that are applied when
	// selecting this completion.
	//
	// Additional text edits should be used to change text unrelated to the current cursor offset
	// (for example adding an import statement at the top of the file if the completion item will
	// insert an unqualified type).
	AdditionalTextEdits []protocol.TextEdit

	// depth is how many levels were searched to find this completion.
	// For example when completing "foo<>", "fooBar" is depth 0, and
	// "fooBar.Baz" is depth 1.
	Depth int

	// Score is the internal relevance score.
	// A higher score indicates that this completion item is more relevant.
	Score float64

	// snippet is the LSP snippet for the completion item. The LSP
	// specification contains details about LSP snippets. For example, a
	// snippet for a function with the following signature:
	//
	//     func foo(a, b, c int)
	//
	// would be:
	//
	//     foo(${1:a int}, ${2: b int}, ${3: c int})
	//
	// If Placeholders is false in the CompletionOptions, the above
	// snippet would instead be:
	//
	//     foo(${1:})
	snippet *snippet.Builder

	// Documentation is the documentation for the completion item.
	Documentation string
}

// Snippet is a convenience returns the snippet if available, otherwise
// the InsertText.
// used for an item, depending on if the callee wants placeholders or not.
func (i *CompletionItem) Snippet() string {
	if i.snippet != nil {
		return i.snippet.String()
	}
	return i.InsertText
}

func computeWordRange(sourceFile *ast.SourceFile, offset int) (span.UTF16Span, error) {
	isWord := func(ch rune) bool {
		return scanner.IsIdentifierStart(ch, ast.Java8) ||
			scanner.IsIdentifierPart(ch, ast.Java8)
	}

	start := offset
	for start > 0 {
		ch, size := utf8.DecodeLastRune(sourceFile.Text[:start])
		if isWord(ch) {
			start -= size
			continue
		}
		break
	}

	end := offset
	for end < len(sourceFile.Text) {
		ch, size := utf8.DecodeRune(sourceFile.Text[end:])
		if isWord(ch) {
			end += size
			continue
		}
		break
	}

	return span.UTF16SpanFromOffsetWithCache(start, end, sourceFile.Text, sourceFile.GetLineStarts())
}

func getCompletionEntriesFromSymbols(symbols []*binder.Symbol, location ast.Node, performCharacterChecks bool, typeChecker checker.TypeChecker, target ast.ScriptTarget) ([]CompletionItem, map[string]string) {
	var entries []CompletionItem
	var uniqueNames = make(map[string]string)
	for _, symbol := range symbols {
		var createCompletionEntry = func (symbol *binder.Symbol, location ast.Node, performCharacterChecks bool, typeChecker checker.TypeChecker, target ast.ScriptTarget) *CompletionItem {
			// TODO(drosen): PkgName now we just permit *all* semantic meanings when calling
			// 'getSymbolKind' which is permissible given that it is backwards compatible; but
			// really we should consider passing the meaning for the ast so that we don't report
			// that a suggestion for a value is an interface.  We COULD also just do what
			// 'getSymbolModifiers' does, which is to use the first declaration.

			// Use a 'sortText' of 0' so that all symbol completion entries come before any other
			// entries (like JavaScript identifier entries).
			return &CompletionItem{
				Label:               symbol.Name,
				InsertText:          symbol.Name,
				Kind:                getSymbolKind(typeChecker, symbol, location),
			}
		}

		var entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target)
		if entry != nil {
			var id = entry.Label
			if _, has := uniqueNames[id]; !has {
				entries = append(entries, *entry)
				uniqueNames[id] = id
			}
		}
	}
	return entries, uniqueNames
}

func (s *session) GetCompletionsAtPosition(ctx context.Context, uri xpath.URI, pos span.UTF16Position) ([]CompletionItem, span.UTF16Span, error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var sourceFile = s.program.GetSourceFile(uri)
	if sourceFile == nil {
		return nil, span.UTF16Span{}, errors.New("Unknown file " + uri.Filename())
	}

	// Convert rune pos to byte offset
	offset, err := pos.ToOffsetWithCache(sourceFile.Text, sourceFile.GetLineStarts())
	if err != nil {
		return nil, span.UTF16Span{}, err
	}

	rgn, err := computeWordRange(sourceFile, offset)
	if err != nil {
		return nil, span.UTF16Span{}, err
	}

	var typeChecker = s.program.GetTypeChecker()
	var completionData = getCompletionData(typeChecker, sourceFile, offset)
	if completionData == nil {
		return nil, span.UTF16Span{}, nil
	}
	items, _ := getCompletionEntriesFromSymbols(completionData.symbols, nil, true, typeChecker, ast.Java8)
	if !completionData.isMemberCompletion {
		items = append(items, keywordCompletions ...)
	}
	return items, rgn, nil
}